# uncompyle6 version 3.2.3
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.6.8 |Anaconda custom (64-bit)| (default, Feb 21 2019, 18:30:04) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: api\handlers\frontend\account.py
__author__ = "Attila Gerendi (Sanyi)"
import logging, time, uuid, os.path, json, datetime, random
from sqlalchemy import and_ as sql_and
from sqlalchemy import desc as sql_desc
from sqlalchemy import func as sql_func
from sqlalchemy import not_ as sql_not
from sqlalchemy import select as sql_select
from sqlalchemy.dialects.postgresql import array
import sqlalchemy.exc
from sqlalchemy.sql import column
from werkzeug.http import dump_cookie
from dateutil.tz import tzutc
from api.classes.frontend import *
from api.classes.frontend.auto import ClsProfile
from api.handlers.frontend import FrontendResourceHandler
from db import Connection
from db.data.access import get_distinct_targets_id_by_group
from db.data.users import (
    get_user_shard,
    prepare_user_data,
    get_owner_id,
    update_license,
)
from db.data.users import is_master_user
from db.tables.events import create_event
from db.tables.scan_sessions import ScanSessionRow, ScanSessionsTable
from db.tables.scans import ScanRow, ScansTable
from db.tables.target_vulns import TargetVulnRow, TargetVulnsTable
from db.tables.target_vulns_stats import TargetVulnsStatsRow, TargetVulnsStatsTable
from db.tables.targets import TargetsTable
from db.tables.users import UsersTable, UserRow, UserConfirmation
from helpers.emails import is_email_banned, is_email_untrusted
from helpers.intents import (
    create_email_change_intent,
    create_password_reset_intent,
    create_new_account_intent,
    create_otp_reset_intent,
)
from helpers.licensing.features import Features
from helpers.mailer.base_mailer import Mailer
from helpers.rate_limiter import RateLimiter
from server.http_responses.http_errors import Http409, Http404, Http500, ApiErrors
from server.routers.api.auth import (
    AdminImpersonatorAuth,
    InvalidAuth,
    ApiKeyAuth,
    UiSessionAuth,
)
from server.http_responses.http_errors import TooManyRequests
from server.http_responses.http_status_codes import Http204
from settings_provider import settings
from helpers.setup_types import *
from helpers.hashing import hmac256hex
from db.tables.licenses import LicenseRow, LicensesTable, LicenseStatus
from db.data.users import require_master_user
from db.tables.targets import TargetRow, TargetTypes
from db.tables.phone_validation import PhoneValidationRow, PhoneValidationTable
from helpers.licensing.erp import activate_license
from helpers.json_extra import JsonEncoder
from db.tables.targets_configurations import (
    TargetConfigurationTable,
    TargetConfigurationRow,
)
from db.tables.scan_session_jobs import ScanSessionJobsTable, ScanSessionJobRow
from helpers.hashing import sha256
from helpers.otp import get_totp_tokens
from helpers.account import (
    require_password_is_not_reused,
    update_account_security,
    get_account_security_settings,
)
from helpers.account import AccountLockout, reset_ui_sessions
from helpers.http_utils import http_date
from db.tables.ui_sessions import UiSessionRow, UiSessionsTable
from helpers.constants.scans import ScanStatusTypes
from helpers.licensing.features import BaseLicense

logger = logging.getLogger("frontend_api_resources.User")
phone_call_validation_requests = dict()


class Account(FrontendResourceHandler):
    def logout(self, **_):
        """
        authenticated
        :param _:
        :return:
        """
        cookie = dump_cookie("ui_session", expires=0, max_age=0)
        with Connection(self.current_shard_name) as (db):
            if self.request_auth.admin_id is None:
                event = create_event(
                    "account_logout",
                    get_owner_id(self.request_user),
                    self.request_user["user_id"],
                    shard=self.current_shard_name,
                )
                db.execute(event)
            q = (
                UiSessionsTable.delete()
                .where(UiSessionRow.key == self.request_auth.key)
                .where(UiSessionRow.user_id == self.request_user_id)
            )
            db.execute(q)
        return Http204(cookie=cookie)

    def login(self, body, **_):
        """
        Not authenticated
        :param body:
        :param _:
        :return:
        """
        login = ClsUiUserLogin(**body)
        logger.debug("login in: %s", login.email)
        rt_login_brute_force = settings.get("rt_login_brute_force")
        if rt_login_brute_force:
            r = rt_login_brute_force.wait_time(login.email)
            if r:
                raise TooManyRequests(r)
        shard_name = get_user_shard(email=login.email)
        if shard_name is None:
            raise InvalidAuth()
        with Connection(shard_name) as (db):
            admin_login = None
            user_license = None
            if is_setup_type_aws() and len(login.password) == 225:
                try:
                    admin_login = AdminImpersonatorAuth(login.password)
                except InvalidAuth as e:
                    logger.exception("invalid admin login %s", e)
                    raise
                else:
                    q = (
                        sql_select((UserRow.extra, UserRow.password))
                        .where(UserRow.user_id == admin_login.admin_id)
                        .where(UserRow.deleted_at.is_(None))
                        .where(UserRow.enabled.is_(True))
                    )
                    admin = db.execute(q).fetchone()
                    if not admin:
                        raise InvalidAuth()
                    admin_extra = admin.extra.get("admin")
                    if not admin_extra:
                        raise InvalidAuth()
                    if not admin_extra.get("impersonator"):
                        raise InvalidAuth()
                    if admin_login.admin_auth != sha256(
                        admin.password, UiSessionAuth.SESSION_SECRET
                    ):
                        raise InvalidAuth()

                user = prepare_user_data(db, email=login.email)
                if not user:
                    raise InvalidAuth()
                lockout_settings = user.get("lockout")
                if admin_login:
                    if admin_login.user_id != user["user_id"].replace("-", ""):
                        raise InvalidAuth()
            if user["license_key"]:
                if is_setup_type_aws():
                    update_license(user["license_key"], shard_name)
                else:
                    if lockout_settings:
                        if lockout_settings["count"]:
                            locked = AccountLockout.is_account_locked(user["email"])
                            if locked:
                                retry_after = int(locked - time.time())
                                date = http_date(
                                    datetime.datetime.now(tzutc())
                                    + (datetime.timedelta(minutes=retry_after))
                                )
                                raise InvalidAuth(
                                    code=ApiErrors.ACCOUNT_LOCKED,
                                    details=dict(
                                        retry_after=date, delay_seconds=retry_after
                                    ),
                                )
                    if not user["password"]:
                        raise InvalidAuth(
                            "no password", code=ApiErrors.ACCOUNT_HAS_NO_PASSWORD
                        )
                    if user["confirmation"] == UserConfirmation.REJECTED:
                        raise InvalidAuth("rejected", code=ApiErrors.ACCOUNT_REJECTED)
                    if user["license_key"]:
                        if is_setup_type_aws():
                            update_license(user["license_key"], shard_name)
                    if not user["enabled"]:
                        raise InvalidAuth(
                            "user disabled", code=ApiErrors.ACCOUNT_DISABLED
                        )
                    ph = settings["password_hashing_strategy"]
                    if (
                        ph(login.password, settings.get("password_hash_salt"))
                        != user["password"]
                    ):
                        if lockout_settings:
                            if lockout_settings["count"]:
                                AccountLockout.report_bad_login(
                                    email=user["email"],
                                    lockout_time=lockout_settings["time"] * 60,
                                    lockout_window=lockout_settings["window"] * 60,
                                    lockout_count=lockout_settings["count"],
                                )
                        rt_login_brute_force.add(login.email)
                        raise InvalidAuth()
                    user_license = BaseLicense.get_license_from_license_data(
                        user["license_data"]
                    )
                    if not is_master_user(user):
                        user_license.require(Features.MULTI_USER)
                if user["otp"]:
                    totp = login.get("totp")
                    if totp is None:
                        raise InvalidAuth(code=ApiErrors.OTP_REQUIRED_BUT_NOT_SENT)
                    if totp not in get_totp_tokens(user["otp"], 1):
                        if lockout_settings:
                            AccountLockout.report_bad_login(
                                email=user["email"],
                                lockout_time=lockout_settings["time"] * 60,
                                lockout_window=lockout_settings["window"] * 60,
                                lockout_count=lockout_settings["count"],
                            )
                        rt_login_brute_force.add(login.email)
                        raise InvalidAuth()
                    else:
                        if user["otp_required"]:
                            raise InvalidAuth(code=ApiErrors.OTP_REQUIRED_BUT_NOT_SET)
                    if user["pwd_expires"]:
                        if user["pwd_expires"] < datetime.datetime.now(tzutc()):
                            raise InvalidAuth(code=ApiErrors.PASSWORD_EXPIRED)
            if not settings.get("features.multi_sessions_enabled", False):
                if login.get("logout_previous"):
                    q = UiSessionsTable.delete().where(
                        UiSessionRow.user_id == user["user_id"]
                    )
                    db.execute(q)
                else:
                    q = (
                        sql_select((sql_func.count(UiSessionRow.user_id),))
                        .where(UiSessionRow.user_id == user["user_id"])
                        .where(UiSessionRow.expires > sql_func.current_timestamp())
                        .where(sql_not(UiSessionRow.key.like("3%")))
                    )
                    c = db.execute(q).scalar()
                    if c:
                        raise InvalidAuth(
                            code=ApiErrors.MULTIPLE_SESSIONS_NOT_SUPPORTED
                        )
                if random.randrange(0, 100) < 10:
                    q = UiSessionsTable.delete().where(
                        UiSessionRow.expires < sql_func.current_timestamp()
                    )
                    db.execute(q)
                if admin_login:
                    session_max_age = admin_login.expiration - time.time()
                    cookie = dump_cookie(
                        "ui_session",
                        admin_login.key,
                        httponly=True,
                        max_age=session_max_age,
                    )
                    session_key = admin_login.key
                else:
                    if not user_license.can_license_log_in():
                        if user_license.is_poc_license():
                            raise InvalidAuth(code=ApiErrors.LICENSE_POC_EXPIRED)
                        else:
                            raise InvalidAuth(code=ApiErrors.LICENSE_EXPIRED)
                        event = create_event(
                            "account_login",
                            get_owner_id(user),
                            user["user_id"],
                            data=dict(ip=self.remote_address),
                            shard=shard_name,
                        )
                        db.execute(event)
                        if user["max_session"]:
                            session_max_age = user["max_session"] * 60
                        else:
                            session_max_age = 691200
                        session_key = (
                            UiSessionAuth.create(
                                user["user_id"],
                                expiration=time.time() + session_max_age,
                            )
                        ).key
                        if login.remember_me:
                            cookie = dump_cookie(
                                "ui_session",
                                session_key,
                                max_age=session_max_age,
                                httponly=True,
                                secure=settings.get("secure_cookie", True),
                            )
                        else:
                            cookie = dump_cookie(
                                "ui_session",
                                session_key,
                                httponly=True,
                                secure=settings.get("secure_cookie", True),
                            )
                        q = UiSessionsTable.insert().values(
                            session_id=str(uuid.uuid4()),
                            key=session_key,
                            user_id=user["user_id"],
                            src=self.remote_address,
                            expires=sql_func.current_timestamp()
                            + (datetime.timedelta(seconds=session_max_age)),
                        )
                        db.execute(q)
                        return Http204(headers={"X-Auth": session_key}, cookie=cookie)

    @staticmethod
    def register_new_user(body, **_):
        """
        Takes new user data and creates an intent "new_account"
        :param body:
        :param _:
        :return:
        """
        setup_type = settings["setup_type"]
        if setup_type != AWS:
            logger.warning("invalid setup type")
            raise Http404()
        mailer = settings.get("mailer_object")
        if not mailer:
            raise Http409("There is no way set to deliver emails")
        user_data = ClsUserRegistrationData(**body)
        if user_data.request_sign != hmac256hex(
            str(user_data.request_id), settings["registration_shared_secret"]
        ):
            raise Http409("invalid signature")
        email = user_data.email.lower()
        if has_feature(SetupFeatures.EMAIL_FILTERING):
            if is_email_banned(email):
                raise Http409("invalid email address")
        intent_id = create_new_account_intent(user_data.to_dict())
        if intent_id:
            mailer.enqueue(
                from_address=None,
                data=dict(intent_id=intent_id),
                email=email,
                first_name=user_data.first_name,
                last_name=user_data.last_name,
                subject_template="register_user_subject.txt.jinja2",
                html_template="register_user.html.jinja2",
                txt_template="register_user.txt.jinja2",
                frontend_url=settings.get("frontend_url"),
            )
        return Http204()

    def change_credentials(self, body, **_):
        """
        Double authenticated request
        :param body:
        :param _:
        :return:
        """
        credentials_change_data = ClsCredentialsChangeData(**body)
        changes = dict()
        ph = settings["password_hashing_strategy"]
        current_password = ph(
            credentials_change_data.current_password, settings.get("password_hash_salt")
        )
        if self.request_user["password"] != current_password:
            raise InvalidAuth()
        if credentials_change_data.get("new_password") is not None:
            if not credentials_change_data.get("current_password"):
                raise Http409("invalid current password")
            new_password = ph(
                credentials_change_data.new_password, settings.get("password_hash_salt")
            )
            if new_password == self.request_user["password"]:
                if self.request_user["pwd_history"]:
                    raise Http409(
                        "password reused", code=ApiErrors.PASSWORD_REUSE_ERROR
                    )
            else:
                with Connection(self.current_shard_name) as (db):
                    require_password_is_not_reused(
                        db,
                        self.request_user_id,
                        self.request_user["pwd_history"],
                        new_password,
                        current_password,
                    )
                if self.request_user["pwd_max_age"]:
                    changes["pwd_expires"] = datetime.datetime.now(tzutc()) + (
                        datetime.timedelta(days=self.request_user["pwd_max_age"])
                    )
                else:
                    changes["pwd_expires"] = None
                changes["password"] = new_password
            if credentials_change_data.get("new_email") is not None:
                if credentials_change_data.new_email != self.request_user["email"]:
                    changes["email"] = credentials_change_data.new_email.lower()
            if changes:
                with Connection(self.current_shard_name) as (db):
                    if "email" in changes:
                        if has_feature(SetupFeatures.EMAIL_FILTERING):
                            if is_email_banned(changes["email"]) or is_email_untrusted(
                                changes["email"]
                            ):
                                raise Http409("unsupported email domain")
                        mailer = settings.get("mailer_object")
                        if not mailer or not mailer.sender.can_send():
                            raise Http409("There is no way set to deliver emails")
                        intent_id = create_email_change_intent(
                            self.request_user["user_id"],
                            self.request_user["email"],
                            changes["email"],
                        )
                        event = create_event(
                            "account_email_change_asked",
                            get_owner_id(self.request_user),
                            self.request_user["user_id"],
                            data=dict(new_email=changes["email"], intent_id=intent_id),
                            shard=self.current_shard_name,
                        )
                        db.execute(event)
                    if "password" in changes:
                        v = dict(password=changes["password"])
                        if self.request_user["pwd_max_age"]:
                            v["pwd_expires"] = datetime.datetime.now(tzutc()) + (
                                datetime.timedelta(
                                    days=self.request_user["pwd_max_age"]
                                )
                            )
                        else:
                            v["pwd_expires"] = None
                        q = (
                            (UsersTable.update(values=v))
                            .where(
                                UserRow.password
                                == ph(
                                    credentials_change_data.current_password,
                                    settings.get("password_hash_salt"),
                                )
                            )
                            .where(UserRow.user_id == self.request_user_id)
                        )
                        db.execute(q)
                        event = create_event(
                            "account_password_changed",
                            get_owner_id(self.request_user),
                            self.request_user["user_id"],
                            data=None,
                            shard=self.current_shard_name,
                        )
                        db.execute(event)
                        reset_ui_sessions(db, self.request_user_id)
            return Http204()

    def get_account_security_settings(self, **_):
        return get_account_security_settings(self.request_user)

    def modify_account_security_settings(self, body, **_):
        self.requires_master_user()
        data = ClsAccountSecurity(ignored_required_keys=True, **body)
        user = self.request_user
        with Connection(self.current_shard_name) as (db):
            update_account_security(db, user, data)
            if data.get("totp_enabled") is False:
                if user["otp"]:
                    q = create_event(
                        "account_otp_removed",
                        self.request_owner_id,
                        self.request_user_id,
                        shard=self.current_shard_name,
                    )
                    db.execute(q)
            return Http204()

    @staticmethod
    def update_password(body, **_):
        """
        Used by user to set up a new OTP secret for account,
        this request is session less
        :param body:
        :param _:
        :return:
        """
        data = ClsUpdatePasswordData(**body)
        rt_login_brute_force = settings.get("rt_login_brute_force")
        if rt_login_brute_force:
            r = rt_login_brute_force.wait_time(data.email)
            if r:
                raise TooManyRequests(r)
        shard_name = get_user_shard(email=data.email)
        if shard_name is None:
            raise InvalidAuth()
        with Connection(shard_name) as (db):
            user = prepare_user_data(db, email=data.email)
            if not user:
                raise InvalidAuth()
            ph = settings["password_hashing_strategy"]
            new_password = ph(data.new_password, settings.get("password_hash_salt"))
            current_password = ph(
                data.current_password, settings.get("password_hash_salt")
            )
            if user["password"] != current_password:
                rt_login_brute_force.add(data.email)
                raise InvalidAuth()
            if new_password == current_password:
                raise Http409("password reused", code=ApiErrors.PASSWORD_REUSE_ERROR)
            changes = dict()
            require_password_is_not_reused(
                db, user["user_id"], user["pwd_history"], new_password, current_password
            )
            changes["password"] = new_password
            if user["pwd_max_age"]:
                changes["pwd_expires"] = datetime.datetime.now(tzutc()) + (
                    datetime.timedelta(days=user["pwd_max_age"])
                )
            q = (UsersTable.update(values=changes)).where(
                UserRow.user_id == user["user_id"]
            )
            db.execute(q)
            q = create_event(
                "account_password_changed",
                get_owner_id(user),
                user["user_id"],
                data=None,
                shard=shard_name,
            )
            db.execute(q)
            reset_ui_sessions(db, user["user_id"])
        return Http204()

    @staticmethod
    def otp_set(body, **_):
        """
        Used by user to set up a new OTP secret for account,
        this request is session less
        :param body:
        :param _:
        :return:
        """
        data = ClsCreateOtpData(**body)
        rt_login_brute_force = settings.get("rt_login_brute_force")
        if rt_login_brute_force:
            r = rt_login_brute_force.wait_time(data.email)
            if r:
                raise TooManyRequests(r)
        shard_name = get_user_shard(email=data.email)
        if shard_name is None:
            raise InvalidAuth()
        with Connection(shard_name) as (db):
            user = prepare_user_data(db, email=data.email)
            if not user:
                raise InvalidAuth()
            ph = settings["password_hashing_strategy"]
            if (
                ph(data.password, settings.get("password_hash_salt"))
                != user["password"]
            ):
                rt_login_brute_force.add(data.email)
                raise InvalidAuth()
            if user["otp"]:
                raise Http409("otp already set")
            tokens = get_totp_tokens(data.otp_secret, 2)
            try:
                p1 = tokens.index(data.totp_pwd_1)
                p2 = tokens.index(data.totp_pwd_2)
            except ValueError:
                raise Http409("invalid otp")

            if p1 >= p2:
                raise Http409("invalid otp (not consecutive)")
            q = (UsersTable.update(values=dict(otp=data.otp_secret))).where(
                UserRow.user_id == user["user_id"]
            )
            db.execute(q)
            if user["owner_id"] is None:
                owner_id = user["user_id"]
            else:
                owner_id = user["user_id"]
            q = create_event(
                "account_otp_set", owner_id, user["user_id"], shard=shard_name
            )
            db.execute(q)
        return Http204()

    def otp_reset(self, body, **_):
        data = ClsOtpResetData(**body)
        ph = settings["password_hashing_strategy"]
        auth_password = ph(data.password, settings.get("password_hash_salt"))
        if self.request_user["email"] != data.email:
            raise InvalidAuth()
        if self.request_user["password"] != auth_password:
            raise InvalidAuth()
        with Connection(self.current_shard_name) as (db):
            q = (UsersTable.update(values=dict(otp=None))).where(
                UserRow.user_id == self.request_user_id
            )
            db.execute(q)
            event_query = create_event(
                "account_otp_reset",
                self.request_owner_id,
                self.request_user_id,
                shard=self.current_shard_name,
                data=dict(remote_address=self.remote_address),
            )
            db.execute(event_query)
        return Http204()

    @staticmethod
    def otp_disable_by_email(body, **_):
        """
        Used by user to ask for OTP reset via email intent,
        this request is session less
        :param body:
        :param _:
        :return:
        """
        data = ClsOtpResetData(**body)
        shard_name = get_user_shard(email=data.email)
        if shard_name is None:
            return Http204()
        else:
            ph = settings["password_hashing_strategy"]
            auth_password = ph(data.password, settings.get("password_hash_salt"))
            with Connection(shard_name) as (db):
                user = prepare_user_data(db, email=data.email)
                if not user:
                    return Http204()
                if user["password"] != auth_password:
                    return Http204()
                if user["otp"]:
                    intent_id = create_otp_reset_intent(user["user_id"])
                    if user["owner_id"] is None:
                        owner_id = user["user_id"]
                    else:
                        owner_id = user["owner_id"]
                    event_query = create_event(
                        "account_otp_reset_asked",
                        owner_id,
                        user["user_id"],
                        shard=shard_name,
                        data=dict(intent_id=intent_id, email=data.email),
                    )
                    db.execute(event_query)
            return Http204()

    def delete_master_account(self, **_):
        if not self.request_user:
            raise AssertionError
        require_master_user(self.request_user)
        delete_user_query = (
            UsersTable.update(
                values=dict(
                    deleted_at=sql_func.current_timestamp(),
                    email="##master_%s##%s"
                    % (int(time.time()), self.request_user["email"]),
                )
            )
        ).where(UserRow.user_id == self.request_user["user_id"])
        reset_any_next_run = (ScansTable.update(values=dict(next_run=None))).where(
            ScanRow.owner_id == self.request_user["user_id"]
        )
        targets_query = sql_select((TargetRow.target_id,)).where(
            TargetRow.owner_id == self.request_user["user_id"]
        )
        reset_continuous_mode = (
            (TargetConfigurationTable.update(values=dict(value=None)))
            .where(TargetConfigurationRow.target_id.in_(targets_query))
            .where(TargetConfigurationRow.name == "continuous_mode")
        )
        sf = ScanSessionsTable.join(
            ScansTable, ScanRow.scan_id == ScanSessionRow.scan_id
        )
        scan_session_query = (
            sql_select((ScanSessionRow.scan_session_id,))
            .select_from(sf)
            .where(ScanRow.owner_id == self.request_user["user_id"])
        )
        abort_any_ongoing_scans = (
            (ScanSessionJobsTable.update(values=dict(abort_requested=True)))
            .where(
                ScanSessionJobRow.status.in_(
                    (
                        ScanStatusTypes.QUEUED,
                        ScanStatusTypes.STARTING,
                        ScanStatusTypes.PROCESSING,
                        ScanStatusTypes.RESUMING,
                    )
                )
            )
            .where(ScanSessionJobRow.scan_session_id.in_(scan_session_query))
        )
        with Connection(self.current_shard_name) as (db):
            db.execute(delete_user_query)
            db.execute(reset_any_next_run)
            db.execute(reset_continuous_mode)
            db.execute(abort_any_ongoing_scans)
        return Http204()

    def update_profile(self, body, **_):
        """
        Patch
        :param body:
        :param _:
        :return:
        """
        if not self.request_user:
            raise AssertionError
        require_master_user(self.request_user)
        user_change_data = ClsProfile(
            ignored_required_keys=True, ignore_default_values=True, **body
        )
        changes = dict()
        try:
            if user_change_data.notifications != self.request_user["notifications"]:
                changes["notifications"] = user_change_data.notifications
        except AttributeError:
            pass

        try:
            if user_change_data.notifications != self.request_user["notifications"]:
                changes["notifications"] = user_change_data.notifications
        except AttributeError:
            pass

        try:
            if user_change_data.country != self.request_user["country"]:
                changes["country"] = user_change_data.country
        except AttributeError:
            pass

        try:
            if user_change_data.last_name != self.request_user["last_name"]:
                changes["last_name"] = user_change_data.last_name
        except AttributeError:
            pass

        try:
            if user_change_data.first_name != self.request_user["first_name"]:
                changes["first_name"] = user_change_data.first_name
        except AttributeError:
            pass

        try:
            if user_change_data.website != self.request_user["website"]:
                changes["website"] = user_change_data.website
        except AttributeError:
            pass

        try:
            if user_change_data.phone != self.request_user["phone"]:
                changes["phone"] = user_change_data.phone
        except AttributeError:
            pass

        try:
            if user_change_data.company != self.request_user["company"]:
                changes["company"] = user_change_data.company
        except AttributeError:
            pass

        if changes:
            stmt = (UsersTable.update(values=changes)).where(
                UserRow.user_id == self.request_user["user_id"]
            )
            event = create_event(
                "account_details_modified",
                get_owner_id(self.request_user),
                self.request_user["user_id"],
                data=changes,
                shard=self.current_shard_name,
            )
            with Connection(self.current_shard_name) as (db_engine):
                db_engine.execute(stmt)
                db_engine.execute(event)
        if changes:
            if has_feature(SetupFeatures.SALES_UPDATES):
                if is_master_user(self.request_user):
                    from helpers.sales.events import user_data_changed

                    user_data = dict(self.request_user)
                    user_data.update(changes)
                    user_data_changed(user_data, self.remote_address)
        return Http204()

    def get_profile(self, **_):
        """
        /api/v1/me returns the user profile
        :param _:
        :return:
        """
        if not self.request_user:
            raise AssertionError
        profile = (ClsProfile(ignore_null_values=True, **self.request_user)).to_dict()
        try:
            profile["su"] = self.request_user["system_user"]
        except KeyError:
            pass

        return profile

    @classmethod
    def password_reset(cls, body, **_):
        setup_type = settings["setup_type"]
        if setup_type != AWS:
            raise Http404()
        mailer = settings.get("mailer_object")
        if not mailer or not mailer.sender.can_send():
            raise Http409("Can't send emails")
        email = (ClsEmailContainer(**body)).email.lower()
        shard_name = get_user_shard(email=email)
        if not shard_name:
            logger.warning("reset_password: %s user shard not found", email)
            return Http204()
        user_query = (
            sql_select((UserRow.user_id, UserRow.owner_id))
            .where(UserRow.email == email)
            .where(UserRow.enabled.is_(True))
            .where(UserRow.deleted_at.is_(None))
        )
        with Connection(shard_name) as (connection):
            user = connection.execute(user_query).fetchone()
            if not user:
                logger.warning("reset_password: %s user not found", email)
                return Http204()
            intent_id = create_password_reset_intent(user.user_id)
            if user["owner_id"] is None:
                owner_id = user.user_id
            else:
                owner_id = user.owner_id
            event_query = create_event(
                "account_password_reset_asked",
                owner_id,
                user.user_id,
                shard=shard_name,
                data=dict(intent_id=intent_id, email=email),
            )
            connection.execute(event_query)
            return Http204()

    def get_trends(self, **_):
        """
        
        :param _:
        :return:
        """
        self.license.require(Features.TREND_GRAPHS)
        caching_path = settings.get("storage.caching.trends")
        cache_path = None
        if caching_path:
            cache_path = os.path.join(
                caching_path,
                self.request_user["user_id"][0:2],
                self.request_user["user_id"],
            )
            try:
                timestamp = os.path.getmtime(cache_path)
            except FileNotFoundError:
                timestamp = 0

            if time.time() - timestamp < 300:
                with open(cache_path, "r", encoding="utf-8") as (data_file):
                    try:
                        return json.load(data_file)
                    except Exception:
                        pass

        with Connection(self.current_shard_name) as (connection):
            new_vulns_trending = []
            open_vulns_trending = []
            average_remediation_time = []
            average_vuln_age_trending = []
            q = sql_select(
                (
                    column("start_date"),
                    column("end_date"),
                    column("med_vuln_rmd_time"),
                    column("high_vuln_rmd_time"),
                    column("avg_vuln_rmd_time"),
                    column("med_vuln_trending"),
                    column("high_vuln_trending"),
                    column("med_vuln_closed"),
                    column("high_vuln_closed"),
                    column("med_vuln_open"),
                    column("high_vuln_open"),
                    column("high_vuln_open_time"),
                    column("med_vuln_open_time"),
                    column("avg_vuln_open_time"),
                )
            ).select_from(sql_func.wsd_timed_vuln_stats(self.request_user["user_id"]))
            for s in connection.execute(q).fetchall():
                new_vulns_trending.append(
                    ClsStatsNewVulnsTrendingItem(
                        end_date=s.end_date,
                        start_date=s.start_date,
                        med_vulns=s.med_vuln_trending,
                        high_vulns=s.high_vuln_trending,
                        weighted_vulns=3 * s.med_vuln_trending
                        + 5 * s.high_vuln_trending,
                    )
                )
                open_vulns_trending.append(
                    ClsStatsOpenVulnsTrendingItem(
                        end_date=s.end_date,
                        start_date=s.start_date,
                        med_vulns=s.med_vuln_open,
                        high_vulns=s.high_vuln_open,
                    )
                )
                s_med_vuln_rmd_time = s.med_vuln_rmd_time
                if s_med_vuln_rmd_time:
                    s_med_vuln_rmd_time = (
                        s_med_vuln_rmd_time.days + s_med_vuln_rmd_time.seconds / 86400
                    )
                s_high_vuln_rmd_time = s.high_vuln_rmd_time
                if s_high_vuln_rmd_time:
                    s_high_vuln_rmd_time = (
                        s_high_vuln_rmd_time.days + s_high_vuln_rmd_time.seconds / 86400
                    )
                s_average_vuln_rmd_time = s.avg_vuln_rmd_time
                if s_average_vuln_rmd_time:
                    s_average_vuln_rmd_time = (
                        s_average_vuln_rmd_time.days
                        + s_average_vuln_rmd_time.seconds / 86400
                    )
                vuln_fixed_med = s.med_vuln_closed if s.med_vuln_closed else 0
                vuln_fixed_high = s.high_vuln_closed if s.high_vuln_closed else 0
                average_remediation_time.append(
                    ClsStatsAvgRemediationTimeItem(
                        end_date=s.end_date,
                        start_date=s.start_date,
                        med_vuln_days=s_med_vuln_rmd_time,
                        high_vuln_days=s_high_vuln_rmd_time,
                        average_days=s_average_vuln_rmd_time,
                        vuln_fixed=vuln_fixed_med + vuln_fixed_high,
                        vuln_fixed_med=vuln_fixed_med,
                        vuln_fixed_high=vuln_fixed_high,
                    )
                )
                s_med_vuln_open_time = s.med_vuln_open_time
                if s_med_vuln_open_time:
                    s_med_vuln_open_time = (
                        s_med_vuln_open_time.days + s_med_vuln_open_time.seconds / 86400
                    )
                else:
                    s_med_vuln_open_time = 0
                if s_med_vuln_rmd_time:
                    s_med_vuln_open_time += s_med_vuln_rmd_time
                s_high_vuln_open_time = s.high_vuln_open_time
                if s_high_vuln_open_time:
                    s_high_vuln_open_time = (
                        s_high_vuln_open_time.days
                        + s_high_vuln_open_time.seconds / 86400
                    )
                else:
                    s_high_vuln_open_time = 0
                if s_high_vuln_rmd_time:
                    s_high_vuln_open_time += s_high_vuln_rmd_time
                s_average_vuln_open_time = s.avg_vuln_open_time
                if s_average_vuln_open_time:
                    s_average_vuln_open_time = (
                        s_average_vuln_open_time.days
                        + s_average_vuln_open_time.seconds / 86400
                    )
                else:
                    s_average_vuln_open_time = 0
                if s_average_vuln_rmd_time:
                    s_average_vuln_open_time += s_average_vuln_rmd_time
                average_vuln_age_trending.append(
                    ClsStatsAvgVulnAgeItem(
                        end_date=s.end_date,
                        start_date=s.start_date,
                        average_days=s_average_vuln_open_time,
                        high_vuln_days=s_high_vuln_open_time,
                        med_vuln_days=s_med_vuln_open_time,
                    )
                )

        result = (
            ClsTrendsData(
                new_vulns_trending=new_vulns_trending[::-1],
                open_vulns_trending=open_vulns_trending[::-1],
                average_remediation_time=average_remediation_time[::-1],
                average_vuln_age_trending=average_vuln_age_trending[::-1],
            )
        ).to_dict()
        if caching_path:
            try:
                os.mkdir(os.path.join(caching_path, self.request_user["user_id"][0:2]))
            except FileExistsError:
                pass

            with open(cache_path, "w", encoding="utf-8") as (data_file):
                json.dump(result, data_file, cls=JsonEncoder)
        return result

    def get_stats(self, **_):
        """
        :param _:
        :return:
        """
        caching_path = settings.get("storage.caching.stats")
        cache_path = None
        if caching_path:
            cache_path = os.path.join(
                caching_path,
                self.request_user["user_id"][0:2],
                self.request_user["user_id"],
            )
            try:
                timestamp = os.path.getmtime(cache_path)
            except FileNotFoundError:
                timestamp = 0

            if time.time() - timestamp < 300:
                with open(cache_path, "r", encoding="utf-8") as (data_file):
                    try:
                        return json.load(data_file)
                    except Exception:
                        pass

        with Connection(self.current_shard_name) as (connection):
            scans_running_count = 0
            scans_waiting_count = 0
            scans_conducted_count = 0
            if self.request_user["owner_id"] is None:
                target_filter = TargetRow.owner_id == self.request_user["user_id"]
                scans_filter = ScanRow.owner_id == self.request_user["user_id"]
            else:
                if self.request_user["access_all_groups"]:
                    target_filter = TargetRow.owner_id == self.request_user["owner_id"]
                    scans_filter = ScanRow.owner_id == self.request_user["owner_id"]
                else:
                    t = get_distinct_targets_id_by_group(self.request_user)
                    target_filter = TargetRow.target_id.in_(t)
                    scans_filter = ScanRow.target_id.in_(t)
                sf = TargetVulnsStatsTable.join(
                    TargetsTable, TargetRow.target_id == TargetVulnsStatsRow.target_id
                )
                q = (
                    sql_select(
                        (
                            sql_func.sum(TargetVulnsStatsRow.vuln_stats[4]).label(
                                "high"
                            ),
                            sql_func.sum(TargetVulnsStatsRow.vuln_stats[3]).label(
                                "med"
                            ),
                            sql_func.sum(TargetVulnsStatsRow.vuln_stats[2]).label(
                                "low"
                            ),
                        )
                    )
                    .select_from(sf)
                    .where(target_filter)
                    .where(TargetRow.deleted_at.is_(None))
                )
                vc = connection.execute(q).fetchone()
                vuln_count = None
                if vc:
                    vuln_count = ClsStatsVulnCount(**dict(vc))
                vulnerabilities_open_count = 0
                if vc["high"]:
                    vulnerabilities_open_count += vc["high"]
                if vc["low"]:
                    vulnerabilities_open_count += vc["low"]
                if vc["med"]:
                    vulnerabilities_open_count += vc["med"]
                sf = TargetVulnsTable.join(
                    TargetsTable, TargetRow.target_id == TargetVulnRow.target_id
                )
                q = (
                    sql_select(
                        (
                            sql_func.count(TargetVulnRow.severity).label("count"),
                            TargetVulnRow.criticality,
                            TargetVulnRow.severity,
                        )
                    )
                    .select_from(sf)
                    .where(target_filter)
                    .where(TargetRow.deleted_at.is_(None))
                    .where(TargetVulnRow.status == "open")
                    .group_by(TargetVulnRow.criticality, TargetVulnRow.severity)
                )
                stats = dict(critical=None, high=None, normal=None, low=None)
                for vc in connection.execute(q).fetchall():
                    if vc.severity == 0:
                        continue
                    if vc.criticality == 30:
                        if stats["critical"] is None:
                            stats["critical"] = [0, 0, 0]
                        stats["critical"][(vc.severity - 1)] += vc.count
                    else:
                        if vc.criticality == 20:
                            if stats["high"] is None:
                                stats["high"] = [0, 0, 0]
                            stats["high"][(vc.severity - 1)] += vc.count
                        else:
                            if vc.criticality == 10:
                                if stats["normal"] is None:
                                    stats["normal"] = [0, 0, 0]
                                stats["normal"][(vc.severity - 1)] += vc.count
                            else:
                                if vc.criticality == 0:
                                    if stats["low"] is None:
                                        stats["low"] = [0, 0, 0]
                                    stats["low"][(vc.severity - 1)] += vc.count

                if stats["critical"]:
                    stats["critical"] = ClsStatsVulnCount(
                        low=stats["critical"][0],
                        med=stats["critical"][1],
                        high=stats["critical"][2],
                    )
                if stats["high"]:
                    stats["high"] = ClsStatsVulnCount(
                        low=stats["high"][0],
                        med=stats["high"][1],
                        high=stats["high"][2],
                    )
                if stats["normal"]:
                    stats["normal"] = ClsStatsVulnCount(
                        low=stats["normal"][0],
                        med=stats["normal"][1],
                        high=stats["normal"][2],
                    )
                if stats["low"]:
                    stats["low"] = ClsStatsVulnCount(
                        low=stats["low"][0], med=stats["low"][1], high=stats["low"][2]
                    )
                vuln_count_by_criticality = ClsStatsVulnCountByCriticality(**stats)
                sf = TargetsTable.join(
                    TargetVulnsStatsTable,
                    TargetVulnsStatsRow.target_id == TargetRow.target_id,
                )
                q = (
                    sql_select(
                        (
                            TargetRow.target_id,
                            TargetRow.address,
                            TargetRow.criticality,
                            array(
                                (
                                    TargetVulnsStatsRow.vuln_stats[4],
                                    TargetVulnsStatsRow.vuln_stats[3],
                                    TargetVulnsStatsRow.vuln_stats[2],
                                    TargetVulnsStatsRow.vuln_stats[0],
                                )
                            ).label("vulnerabilities"),
                        )
                    )
                    .select_from(sf)
                    .order_by(
                        sql_desc(TargetRow.criticality), sql_desc("vulnerabilities")
                    )
                    .where(
                        sql_not(
                            sql_and(
                                TargetVulnsStatsRow.vuln_stats[4] == 0,
                                TargetVulnsStatsRow.vuln_stats[3] == 0,
                            )
                        )
                    )
                    .where(TargetRow.deleted_at.is_(None))
                    .where(target_filter)
                    .limit(10)
                    .offset(0)
                )
                most_vulnerable_targets = []
                for target in connection.execute(q).fetchall():
                    most_vulnerable_targets.append(
                        ClsStatsVulnerableTargets(
                            criticality=target.criticality,
                            target_id=target.target_id,
                            address=target.address,
                            high_vuln_count=target.vulnerabilities[0],
                            med_vuln_count=target.vulnerabilities[1],
                        )
                    )

                sf = TargetVulnsTable.join(
                    TargetsTable, TargetRow.target_id == TargetVulnRow.target_id
                )
                q = (
                    sql_select(
                        (
                            TargetVulnRow.name,
                            TargetVulnRow.vt_id,
                            TargetVulnRow.severity,
                            sql_func.count(TargetVulnRow.vt_id).label("count"),
                        )
                    )
                    .group_by(
                        TargetVulnRow.vt_id, TargetVulnRow.name, TargetVulnRow.severity
                    )
                    .select_from(sf)
                    .where(TargetRow.deleted_at.is_(None))
                    .where(TargetVulnRow.severity > 1)
                    .where(TargetVulnRow.status == "open")
                    .where(target_filter)
                    .order_by(sql_desc("count"))
                    .limit(10)
                    .offset(0)
                )
                top_vulnerabilities = []
                for vuln in connection.execute(q).fetchall():
                    top_vulnerabilities.append(
                        ClsStatsVulnTypeEntry(
                            name=vuln.name,
                            count=vuln.count,
                            severity=vuln.severity,
                            vt_id=vuln.vt_id,
                        )
                    )

                q = (
                    sql_select(
                        (
                            ScanSessionRow.status,
                            sql_func.count(ScanSessionRow.status).label("count"),
                        )
                    )
                    .group_by(ScanSessionRow.status)
                    .select_from(
                        ScanSessionsTable.join(
                            ScansTable, ScanRow.scan_id == ScanSessionRow.scan_id
                        )
                    )
                    .where(scans_filter)
                    .where(ScanRow.continuous.is_(False))
                    .where(ScanRow.deleted_at.is_(None))
                )
                for scan in connection.execute(q).fetchall():
                    if scan.status in (
                        ScanStatusTypes.STARTING,
                        ScanStatusTypes.PROCESSING,
                        ScanStatusTypes.ABORTING,
                        ScanStatusTypes.RESUMING,
                    ):
                        scans_running_count += scan.count
                    if scan.status in (
                        ScanStatusTypes.COMPLETED,
                        ScanStatusTypes.FAILED,
                        ScanStatusTypes.ABORTED,
                    ):
                        scans_conducted_count += scan.count
                    if scan.status in (ScanStatusTypes.QUEUED,):
                        scans_waiting_count += scan.count

            q = (
                sql_select((sql_func.count(TargetRow.target_id),))
                .where(target_filter)
                .where(TargetRow.deleted_at.is_(None))
            )
            targets_count = connection.execute(q).scalar()
            result = (
                ClsStatsData(
                    vuln_count=vuln_count,
                    vuln_count_by_criticality=vuln_count_by_criticality,
                    most_vulnerable_targets=most_vulnerable_targets,
                    top_vulnerabilities=top_vulnerabilities,
                    scans_running_count=scans_running_count,
                    scans_waiting_count=scans_waiting_count,
                    scans_conducted_count=scans_conducted_count,
                    vulnerabilities_open_count=vulnerabilities_open_count,
                    targets_count=targets_count,
                )
            ).to_dict()
        if caching_path:
            try:
                os.mkdir(os.path.join(caching_path, self.request_user["user_id"][0:2]))
            except FileExistsError:
                pass

            with open(cache_path, "w", encoding="utf-8") as (data_file):
                json.dump(result, data_file, cls=JsonEncoder)
        return result

    def get_api_key(self, **_):
        if not self.request_user:
            raise AssertionError
        self.license.require(Features.API_KEY)
        if is_setup_type_aws():
            if self.request_user["license_type"] == "customer":
                self.license.require_active_license()
            else:
                raise Http409("api key disabled")
        else:
            if not self.request_user["api_key_enabled"]:
                raise Http409("api key disabled")
        with Connection(self.current_shard_name) as (db):
            q = sql_select((UserRow.api_key,)).where(
                UserRow.user_id == self.request_user["user_id"]
            )
            user = db.execute(q).fetchone()
            if not user:
                raise Http404()
            if user.api_key:
                ph = settings["api_key_hashing_strategy"]
                api_key = ph(user.api_key, settings.get("password_hash_salt"))[:32]
                return ClsApiKey(
                    api_key=ApiKeyAuth.create(self.request_user["user_id"], api_key).key
                )
            return ClsApiKey(api_key=None)

    def reset_api_key(self, **_):
        if not self.request_user:
            raise AssertionError
        self.license.require(Features.API_KEY)
        if is_setup_type_aws():
            if self.request_user["license_type"] == "customer":
                self.license.require_active_license()
            else:
                raise Http409("api key disabled")
        else:
            if not self.request_user["api_key_enabled"]:
                raise Http409("api key disabled")
        with Connection(self.current_shard_name) as (db):
            new_api_key = str(uuid.uuid4()).replace("-", "")
            q = (
                (UsersTable.update(values=dict(api_key=new_api_key)))
                .where(UserRow.user_id == self.request_user["user_id"])
                .returning(UserRow.api_key)
            )
            result = db.execute(q).scalar()
            if not result:
                raise Http404()
            ph = settings["api_key_hashing_strategy"]
            api_key = ph(result, settings.get("password_hash_salt"))[:32]
            return ClsApiKey(
                api_key=ApiKeyAuth.create(self.request_user["user_id"], api_key).key
            )

    def delete_api_key(self, **_):
        if not self.request_user:
            raise AssertionError
        with Connection(self.current_shard_name) as (db):
            q = (UsersTable.update(values=dict(api_key=None))).where(
                UserRow.user_id == self.request_user["user_id"]
            )
            db.execute(q)
        return Http204()

    def get_manual_intervention(self, ui_session_id, **_):
        manual_browsing_storage = settings.get("manual_browsing_storage")
        if manual_browsing_storage is None:
            return ClsManualInterventionList(values=[])
        else:
            return ClsManualInterventionList(
                values=manual_browsing_storage.get_user_events(
                    self.request_user["user_id"], ui_session_id
                )
            )

    def add_license(self, body, **_):
        """
        this function is only for aws
        :param body:
        :param _:
        :return:
        """
        if not is_setup_type_aws():
            raise Http404()
        require_master_user(self.request_user)
        rt_license_key_brute_force = settings.get("rt_license_key_brute_force")
        if rt_license_key_brute_force:
            r = rt_license_key_brute_force.wait_time(self.request_user["user_id"])
            if r:
                raise TooManyRequests(r)
        license_key = (ClsLicenseKey(**body)).license_key
        if self.request_user["license_type"] == "customer":
            if self.request_user["license_key"] == license_key:
                update_license(
                    self.request_user["license_key"], self.current_shard_name
                )
                return Http204()
            if self.request_user["license_data"]["status"] not in (
                LicenseStatus.EXPIRED,
                LicenseStatus.NOT_FOUND,
            ):
                raise Http409("can't swap license")
        q = sql_select((UserRow.user_id,)).where(UserRow.license_key == license_key)
        with Connection(self.current_shard_name) as (db):
            user_id = db.execute(q).scalar()
            if user_id:
                if rt_license_key_brute_force:
                    rt_license_key_brute_force.add(self.request_user["user_id"])
                logger.warning(
                    "attempting license key reuse %s:%s",
                    self.request_user["user_id"],
                    license_key,
                )
                raise Http409("invalid license key")
        try:
            license_data = activate_license(
                settings["erp_url"], license_key, self.request_user
            )
        except Exception as e:
            logger.exception("fetch license %s failed with: %s", license_key, e)
            raise Http500("internal error")

        if license_data["status"] not in (
            LicenseStatus.OK,
            LicenseStatus.NOT_ACTIVATED,
        ):
            logger.warning("invalid license key status %s", license_data)
            if rt_license_key_brute_force:
                rt_license_key_brute_force.add(self.request_user["user_id"])
            raise Http409("invalid license key")
        with Connection(self.current_shard_name) as (db):
            q = (
                sql_select((sql_func.count(TargetRow.target_id),))
                .where(TargetRow.owner_id == self.request_user["user_id"])
                .where(TargetRow.type == TargetTypes.DEFAULT)
                .where(TargetRow.deleted_at.is_(None))
            )
            c = db.execute(q).scalar()
            if c:
                if c > license_data["licenses"]:
                    raise Http409("too many targets")
            q = (
                sql_select((sql_func.count(UserRow.user_id),))
                .where(UserRow.owner_id == self.request_user["user_id"])
                .where(UserRow.deleted_at.is_(None))
            )
            c = db.execute(q).scalar()
            if c:
                if c > license_data["licenses"] * 0.5:
                    raise Http409("too many child users")
            license_data["last_refresh"] = sql_func.now()
            license_data["license_key"] = license_key
            if license_data["status"] == LicenseStatus.NOT_ACTIVATED:
                license_data["status"] = LicenseStatus.OK
                license_data["first_activated"] = sql_func.now()
                license_data["validity_start"] = sql_func.now()
            q = LicensesTable.insert().values(**license_data)
            try:
                db.execute(q)
            except sqlalchemy.exc.IntegrityError as e:
                if "licenses_pkey" in str(e):
                    q = (LicensesTable.update(values=license_data)).where(
                        LicenseRow.license_key == license_key
                    )
                    db.execute(q)
                else:
                    raise

            logger.info("added %s to the system", license_data)
            q = (
                UsersTable.update(
                    values=dict(license_key=license_key, license_type="customer")
                )
            ).where(UserRow.user_id == self.request_user["user_id"])
            db.execute(q)
            update_license(license_key, self.current_shard_name)
            from helpers.sales.events import license_updated

            user_data = dict(self.request_user)
            user_data["license_key"] = license_key
            license_updated(user_data, self.remote_address)
            return Http204()

    def phone_validation_init(self, **_):
        if not is_setup_type_aws():
            raise Http404()
        phone_validation_method = settings.get("phone_validation.method")
        if phone_validation_method is None:
            raise Http404()
        require_master_user(self.request_user)
        if self.request_user["confirmation"] != UserConfirmation.NEEDS_VALIDATION:
            raise Http409("invalid user state")
        from helpers.emails import is_email_untrusted

        if is_email_untrusted(self.request_user["email"]):
            raise Http409("email provider not allowed")
        if (
            not self.request_user["phone"]
            or not self.request_user["company"]
            or not self.request_user["company"]
            or not self.request_user["first_name"]
            or not self.request_user["last_name"]
            or not self.request_user["website"]
            or not self.request_user["country"]
        ):
            raise Http409("incomplete user data")
        if phone_validation_method == "twilio":
            from rq import Queue
            from helpers.phone_validation.tw import code_call

            rq = Queue(name="call_center", connection=settings.get("rq_redis_conn"))
            previous_request_time = phone_call_validation_requests.get(
                self.request_user["user_id"]
            )
            q = sql_select((PhoneValidationRow.data,)).where(
                PhoneValidationRow.user_id == self.request_user["user_id"]
            )
            with Connection(self.current_shard_name) as (db):
                data = db.execute(q).scalar()
                if data:
                    if data["request_count"] > 4:
                        q = (
                            UsersTable.update(
                                values=dict(
                                    confirmation=UserConfirmation.AUTO_VALIDATION_FAILED
                                )
                            )
                        ).where(UserRow.user_id == self.request_user["user_id"])
                        db.execute(q)
                        raise Http409("too many failed requests")
                if previous_request_time:
                    if time.time() - previous_request_time < 60:
                        raise TooManyRequests(60)
                phone_call_validation_requests[
                    self.request_user["user_id"]
                ] = time.time()
            rq.enqueue(
                code_call,
                shard=self.current_shard_name,
                user_id=self.request_user["user_id"],
                result_ttl=60,
            )
        else:
            logger.error(
                "unsupported phone validation method: %s", phone_validation_method
            )
            raise Http404()

    def phone_validation_consume(self, body, **_):
        if not is_setup_type_aws():
            raise Http404()
        phone_validation_method = settings.get("phone_validation.method")
        if phone_validation_method is None:
            raise Http404()
        require_master_user(self.request_user)
        if self.request_user["confirmation"] != UserConfirmation.NEEDS_VALIDATION:
            raise Http409("invalid user state")
        if phone_validation_method == "twilio":
            pv = ClsPhoneValidationCode(**body)
            q = sql_select((PhoneValidationRow.data,)).where(
                PhoneValidationRow.user_id == self.request_user["user_id"]
            )
            verification_counter = 0
            with Connection(self.current_shard_name) as (db):
                phone_validation_data = db.execute(q).scalar()
                if not phone_validation_data:
                    raise Http409("invalid user state")
                phone_validation_data = dict(phone_validation_data)
                if phone_validation_data["code"] != pv.code:
                    phone_validation_data["failed_count"] = (
                        phone_validation_data.get("failed_count", 0) + 1
                    )
                    if phone_validation_data["failed_count"] > 3:
                        q = (
                            UsersTable.update(
                                values=dict(
                                    confirmation=UserConfirmation.AUTO_VALIDATION_FAILED
                                )
                            )
                        ).where(UserRow.user_id == self.request_user["user_id"])
                        db.execute(q)
                    q = (
                        PhoneValidationTable.update(
                            values=dict(data=phone_validation_data)
                        )
                    ).where(PhoneValidationRow.user_id == self.request_user["user_id"])
                    db.execute(q)
                    raise Http409("invalid code")
                if self.request_user["extra"]:
                    verification_counter = self.request_user["extra"].get(
                        "failed_validations", 0
                    )
                else:
                    self.request_user["extra"] = dict()
                self.request_user["extra"]["validated_phone"] = phone_validation_data[
                    "phone"
                ]
                q = (
                    UsersTable.update(
                        values=dict(
                            confirmation=UserConfirmation.NEEDS_ADMIN_VALIDATION,
                            extra=self.request_user["extra"],
                        )
                    )
                ).where(UserRow.user_id == self.request_user["user_id"])
                db.execute(q)
                phone_validation_data["success"] = True
                q = (
                    PhoneValidationTable.update(values=dict(data=phone_validation_data))
                ).where(PhoneValidationRow.user_id == self.request_user["user_id"])
                db.execute(q)
            from helpers.sales.events import user_extra_verification_required

            user_extra_verification_required(
                self.request_user,
                request_address=self.remote_address,
                verification_counter=verification_counter,
            )
            return Http204()
        logger.error("unsupported phone validation method: %s", phone_validation_method)
        raise Http404()

    def get_active_sessions(self, **_):
        q = (
            sql_select((UiSessionRow,))
            .where(UiSessionRow.expires > sql_func.current_timestamp())
            .where(UiSessionRow.user_id == self.request_user_id)
            .where(sql_not(UiSessionRow.key.like("3%")))
            .limit(100)
            .order_by(UiSessionRow.created, UiSessionRow.key)
        )
        ui_sessions = []
        with Connection(self.current_shard_name) as (db):
            for s in db.execute(q).fetchall():
                ui_sessions.append(
                    ClsUiSession(
                        session_id=s.session_id,
                        created=s.created,
                        expires=s.expires,
                        remote_address=s.src,
                        is_current=s.key == self.request_auth.key,
                    )
                )

        return ClsUiSessionList(values=ui_sessions)

    def remove_active_session(self, session_id, **_):
        with Connection(self.current_shard_name) as (db):
            q = (
                UiSessionsTable.delete()
                .where(UiSessionRow.session_id == session_id)
                .where(UiSessionRow.user_id == self.request_user_id)
            )
            db.execute(q)
        return Http204()
